<?php
/**
 * Dependency plugin service
 *
 * PHP version 5
 *
 * @category  W
 * @package   Services
 * @author    Olivier Hoareau <olivier@phppro.fr>
 * @copyright 2009 PHPPRO http://www.phppro.fr
 * @license   http://dev.phppro.fr/license PHPPRO
 * @version   SVN: $Id:$
 * @link      http://dev.phppro.fr/projects/wdependency
 */
require_once 'W/W.php';
require_once 'W/Dependency/Plugin/Abstract.php';
require_once 'W/Dependency/Exception/Stop.php';
/**
 * Dependency plugin service
 *
 * @category  W
 * @package   Services
 * @author    Olivier Hoareau <olivier@phppro.fr>
 * @copyright 2009 PHPPRO http://www.phppro.fr
 * @license   http://dev.phppro.fr/license PHPPRO
 * @version   SVN: $Id$
 * @link      http://dev.phppro.fr/projects/wdependency
 */
class W_Dependency_Plugin_Service extends W_Dependency_Plugin_Abstract
{
    /**
     * List of registered plugins
     *
     * @var array
     */
    private $plugins;
    /**
     * Default plugin package
     *
     * @var string
     */
    private $pluginDefaultPackage;
    /**
     * List of plugin method names
     *
     * @var array
     */
    private $pluginMethods;

    /**
     * Constructs a new plugin service
     *
     * @return W_Dependency_Plugin_Interface
     */
    public function __construct ()
    {
        $this->pluginDefaultPackage =
            str_replace('/','_',
                dirname(str_replace('_','/',get_class($this))));

        $this->plugins       = array();
        $this->pluginMethods = array();
    }
    /**
     * Configures the plugin service
     *
     * @param null|array $options the configuration options
     *
     * @return W_Dependency_Plugin_Interface
     */
    public function configure ($options=null)
    {
        parent::configure($options);
        if (true === isset($options['pluginDefaultPackage'])) {
            $this->pluginDefaultPackage = $options['pluginDefaultPackage'];
        }
        if (true === isset($options['pluginMethods'])) {
            $this->pluginMethods = $options['pluginMethods'];
            foreach(array_keys($this->pluginMethods) as $pM) {
                $this->plugins[$pM] = array();
            }
        }
        return $this;
    }
    /**
     * Returns the plugin default package configuration option
     *
     * @return string
     */
    public function getPluginDefaultPackage ()
    {
        return $this->pluginDefaultPackage;
    }
    /**
     * Sets the plugin default package configuration option
     *
     * @param string $pluginDefaultPackage default package
     *
     * @return W_Dependency_Plugin_Interface
     */
    public function setPluginDefaultPackage ($pluginDefaultPackage)
    {
        $this->pluginDefaultPackage = $pluginDefaultPackage;
        return $this;
    }
    /**
     * Adds the specified plugin by type
     *
     * @param string|object $plugin the plugin
     * @param string        $type   the plugin type
     *
     * @return W_Dependency_Plugin_Interface
     *
     * @throws Exception if an error occured
     */
    public function addPlugin ($plugin, $options, $type,$defaultPackage=null)
    {
        if (true === is_array($plugin)) {
            foreach ($plugin as $p) {
                call_user_func(
                    array($this, 'add' . ucfirst($type)),
                    $p,
                    $options,$type,$defaultPackage);
            }
            return $this;
        }
        $noTypeMode = false;
        if (null !== $defaultPackage) {
            $noTypeMode = true;
        }else{
            $defaultPackage = $this->getPluginDefaultPackage();
        }

        if (null === $defaultPackage) {
            throw new RuntimeException('No plugin default package set');
        }
        if (true === is_object($plugin)) {
            $interface = $defaultPackage . (false === $noTypeMode ? '_' . ucfirst($type) : '') . '_Interface';
            if (false === ($plugin instanceof $interface)) {
                throw new RuntimeException(
                    ucfirst($type) .
                    " must implements $interface (found class '" .
                    get_class($plugin) . "')");
            }
        } else {
            if (false === is_string($plugin)) {
                throw new RuntimeException(
                    ucfirst($type) .
                    " must be either class name, type or instance");
            }
            $class    = $plugin;
            $class    = str_replace(array('.' , '-'), '_', $class);
            $oldClass = $class;
            try {
                $class = $defaultPackage . (false === $noTypeMode ? '_' . ucfirst($type) : '') . '_' .
                    ucfirst($class);
                W_W::cl()->loadClass($class);
            } catch (Exception $e) {
                W_W::log()->log($e);
                $class = $oldClass;
                W_W::cl()->loadClass($class);
            }
            $plugin = new $class();
        }
        if (true === method_exists($plugin, 'configure')) {
            $plugin->configure($options);
        }

        if (false === isset($this->pluginMethods[$type])) {
            throw new RuntimeException("No plugin method set for plugin type '$type'");
        }

        if (false === method_exists($plugin,$this->pluginMethods[$type])) {
            throw new RuntimeException(
            	"No method '" . $this->pluginMethods[$type] .
            	"' in plugin '" . get_class($plugin) . "'");
        }
        if (false === isset($this->plugins[$type])) {
            $this->plugins[$type] = array();
        }
        if (false === isset($this->plugins[$type]['*all*'])) {
            $this->plugins[$type]['*all*'] = array();
        }
        $this->plugins[$type]['*all*'][] = $plugin;

        if (true === method_exists($plugin, 'listExpectedEvents')) {
            foreach ($plugin->listExpectedEvents() as $eventType) {
                if ('*all*' === $eventType) {
                    continue;
                }
                if (false === isset($this->plugins[$type][$eventType])) {
                    $this->plugins[$type][$eventType] = array();
                }
                $this->plugins[$type][$eventType][] = $plugin;
            }
        }
        return $this;
    }
    /**
     * Magic method call
     *
     * @param string $name method name
     * @param array  $vars list of parameters
     *
     * @return mixed
     *
     * @throws Exception if an error occured
     */
    public function __call($name,$vars)
    {
        $matches = null;
        if (0 < preg_match('|^add(.+)$|',$name,$matches)) {
            $type = $matches[1];
            $type{0} = strtolower($type{0});
            if (false === isset($this->plugins[$type])) {
                throw new RuntimeException("Unknown plugin type '$type'");
            }
            $vars[] = $type;
            if (3 > count($vars)) {
                throw new RuntimeException("Not enough parameter to add plugin: ".json_encode($vars));
            }
            return call_user_func_array(array($this,'addPlugin'),$vars);
        } elseif (0 < preg_match('|^execute(.+)s$|',$name,$matches)) {
            $type = $matches[1];
            $type{0} = strtolower($type{0});
            if (false === isset($this->plugins[$type])) {
                throw new RuntimeException("Unknown plugin type '$type'");
            }
            return call_user_func_array(
                array($this,'executePlugins'),
                array_unshift($vars,$type));
        }
        throw new RuntimeException(
        	"Unknown method '$name' for class '" .
            get_class($this)."'");
    }
    /**
     * Fires the specified event to the registered listeners
     *
     * @param string      $eventType the event type
     * @param array|mixed $eventData the event data
     *
     * @return array|null list of optional additional item to process
     *
     * @throws Exception if an error occured
     */
    public function fireEvent ($eventType, $eventData = null)
    {
        if (false === isset($this->plugins['listener']['all'])) {
            $this->plugins['listener']['all'] = array();
        }
        if (false === isset($this->plugins['listener'][$eventType])) {
            $this->plugins['listener'][$eventType] = array();
        }
        $listeners = array_merge($this->plugins['listener'][$eventType],$this->plugins['listener']['all']);
        $results = array();
        foreach ($listeners as $listener) {
            $listenerResults = $listener->processEvent($eventType, $eventData);
            if (null !== $listenerResults &&
                true === is_array($listenerResults)) {
                foreach ($listenerResults as $result) {
                    $results[$result] = true;
                }
            }
        }
        return array_keys($results);
    }
    /**
     * Returns the computed data
     *
     * @param  string $type the plugin type
     *
     * @return array the collected data
     *
     * @throws Exception if an error occured
     */
    public function getCollectedPluginsData ($type)
    {
        $pluginsData = array();
        if (false === isset($this->plugins[$type]['*all*'])) {
            return $pluginsData;
        }
        foreach ($this->plugins[$type]['*all*'] as $plugin) {
            $pluginData = array();
            if (true === method_exists($plugin,'getData')) {
                $pluginData = $plugin->getData();
            }
            foreach ($pluginData as $key => $data) {
                if (false === isset($pluginsData[$key])) {
                    $pluginsData[$key] = array();
                }
                $pluginsData[$key] = array_merge($pluginsData[$key], $data);
            }
        }
        return $pluginsData;
    }
    /**
     * Executes all registered plugins of specified type
     * and returns computed data
     *
     * @param string $type plugin type
     * @param mixed  $data execution parameter(s)
     *
     * @return mixed execution result
     *
     * @throws Exception if an error occured
     */
    public function executePlugins ($type, $data, $merge=true)
    {
	if (false !== $merge) { $merge = true; }
        if (false === isset($this->plugins[$type])) {
            $this->fireEvent('exit'.ucfirst($type).'s');
            return isset($data['data']) ? $data['data'] : array();
        }
        $this->fireEvent('enter'.ucfirst($type).'s');
        $errors = array();
        if (false === isset($this->plugins[$type]['*all*'])) {
            $this->fireEvent('exit'.ucfirst($type).'s');
            return isset($data['data']) ? $data['data'] : array();
        }
        $newData = array();
	$stop = false;
	$oldData = $data;
	unset($oldData['data']);
        foreach ($this->plugins[$type]['*all*'] as $plugin) {
            $this->fireEvent('enter'.ucfirst($type), array($type => $plugin));
            try {
                if (false === isset($this->pluginMethods[$type])) {
                    $e = new RuntimeException(
                    	"No method registered for plugin type '$type'");
                    $this->fireEvent('exit'.ucfirst($type).'s',array('exception'=>$e));
                    throw $e;
                }
                $method = $this->pluginMethods[$type];
		$resultData = $plugin->$method($data);
		if (false === $merge) {
			$data = array_merge($oldData,array('data'=>$resultData));
			$newData = $resultData;
		} else {
	                if (true === is_array($resultData)) {
        	            $newData = array_merge($newData, $resultData);
                	}
		}
                $this->fireEvent(
                	'exit'.ucfirst($type),
                    array($type => $plugin));
            } catch (W_Dependency_Exception_Stop $e) {
                if (0 === count($errors)) {
                    $stop = true;
                    break;
                }
            } catch (Exception $e) {
                $this->fireEvent(
                	'exit'.ucfirst($type),
                    array($type => $plugin , 'exception' => $e));
                $errors[] = $e;
            }
	}
        if (0 < count($errors)) {
            $errorString = count($errors) . ' error' .
                (1 < count($errors) ? 's' : '') .
                ' detected when executing '.$type.'s: ';
            foreach ($errors as $error) {
                $errorString .= ($errorString ? PHP_EOL : '') .
                	' * ' . $error->getMessage();
            }
            $exception = new RuntimeException($errorString);
            $this->fireEvent(
            	'exit'.ucfirst($type).'s',
                array('exception' => $exception));
            throw $exception;
        }
        $this->fireEvent('exit'.ucfirst($type).'s');
        if (true === $stop) {
            return 'STOP';
        }
        return $newData;
    }
}
